<template>
  <el-upload
      ref="myUpload"
      class="slice-upload"
      drag
      action="/"
      multiple
      :http-request="handleHttpRequest"
      :on-remove="handleRemoveFile"
  >
    <el-icon class="el-icon--upload"><upload-filled /></el-icon>
    <div class="el-upload__text">
      请拖拽文件到此处或 <em>点击此处上传</em>
    </div>
  </el-upload>
</template>

<script>
import md5 from '@/utils/md5'
import { commonUpload, taskInfo, initTask, preSignUrl, merge } from '@/api/upload'
import { ElNotification } from 'element-plus'
import Queue from 'promise-queue-plus'
import { ref } from 'vue'
// import request from '@/api/request'
import axios from 'axios'

export default {
  name: 'SliceUpload',
  emits: ['uploadSuccess', 'onProgress', 'removeUpload'],
  setup(_props, { emit }) {
    // 文件上传分块任务的队列（用于移除文件时，停止该文件的上传队列） key：fileUid value： queue object
    const fileUploadChunkQueue = ref({}).value

    /**
     * 获取一个上传任务，没有则初始化一个
     */
    const getTaskInfo = async (file) => {
      let task
      const identifier = await md5(file)
      const res = await taskInfo(identifier)
      const { code, data, msg } = res
      if (code === 200) {
        task = data
        if (!task) {
          const initTaskData = {
            identifier,
            fileName: file.name,
            totalSize: file.size,
            chunkSize: 5 * 1024 * 1024
          }
          const { code, data, msg } = await initTask(initTaskData)
          if (code === 200) {
            task = data
          } else {
            ElNotification.error({
              title: '文件上传错误',
              message: msg
            })
          }
        }
      } else {
        ElNotification.error({
          title: '文件上传错误',
          message: msg
        })
      }
      return task
    }
    /**
     * 上传逻辑处理，如果文件已经上传完成（完成分块合并操作），则不会进入到此方法中
     */
    const handleUpload = (file, taskRecord, onProgress) => {
      let lastUploadedSize = 0; // 上次断点续传时上传的总大小
      let uploadedSize = 0 // 已上传的大小
      const totalSize = file.size || 0 // 文件总大小
      let startMs = new Date().getTime(); // 开始上传的时间
      const { exitPartList, chunkSize, chunkNum, fileIdentifier } = taskRecord

      // 获取从开始上传到现在的平均速度（byte/s）
      const getSpeed = () => {
        // 已上传的总大小 - 上次上传的总大小（断点续传）= 本次上传的总大小（byte）
        const intervalSize = uploadedSize - lastUploadedSize
        const nowMs = new Date().getTime()
        // 时间间隔（s）
        const intervalTime = (nowMs - startMs) / 1000
        return intervalSize / intervalTime
      }

      const uploadNext = async (partNumber) => {
        const start = new Number(chunkSize) * (partNumber - 1)
        const end = start + new Number(chunkSize)
        const blob = file.slice(start, end)
        let { code, data } = await preSignUrl({ identifier: fileIdentifier, partNumber: partNumber} )
        if (code === 200 && data) {
          await axios.request({
            url: data,
            method: 'put',
            data: blob,
            headers: { 'Content-Type': 'application/octet-stream' }
          })
          return Promise.resolve({ partNumber: partNumber, uploadedSize: blob.size })
        }
        return Promise.reject(`分片${partNumber}， 获取上传地址失败`)
      }

      /**
       * 更新上传进度
       * @param increment 为已上传的进度增加的字节量
       */
      const updateProcess = (increment) => {
        increment = new Number(increment)
        let factor = 1000; // 每次增加1000 byte
        let from = 0;
        // 通过循环一点一点的增加进度
        while (from <= increment) {
          from += factor
          uploadedSize += factor
          const percent = Math.round(uploadedSize / totalSize * 100).toFixed(2);
          onProgress({percent: percent})
        }

        const speed = getSpeed();
        const remainingTime = speed != 0 ? Math.ceil((totalSize - uploadedSize) / speed) + 's' : '未知'
        console.log('剩余大小：', (totalSize - uploadedSize) / 1024 / 1024, 'mb');
        console.log('当前速度：', (speed / 1024 / 1024).toFixed(2), 'mbps');
        console.log('预计完成：', remainingTime);
      }


      return new Promise(resolve => {
        const failArr = [];
        const queue = Queue(5, {
          "retry": 3,               //Number of retries
          "retryIsJump": false,     //retry now?
          "workReject": function(reason){
            failArr.push(reason)
          },
          "queueEnd": function(){
            resolve(failArr);
          }
        })
        fileUploadChunkQueue[file.uid] = queue
        for (let partNumber = 1; partNumber <= chunkNum; partNumber++) {
          const exitPart = (exitPartList || []).find(exitPart => exitPart.partNumber == partNumber)
          if (exitPart) {
            // 分片已上传完成，累计到上传完成的总额中,同时记录一下上次断点上传的大小，用于计算上传速度
            lastUploadedSize += new Number(exitPart.size)
            updateProcess(exitPart.size)
          } else {
            queue.push(() => uploadNext(partNumber).then(res => {
              // 单片文件上传完成再更新上传进度
              updateProcess(res.uploadedSize)
            }))
          }
        }
        if (queue.getLength() == 0) {
          // 所有分片都上传完，但未合并，直接return出去，进行合并操作
          resolve(failArr);
          return;
        }
        queue.start()
      })
    }

    // 正常上传
    const upload = fileBinary => {
      return new Promise((resolve, reject) => {
        commonUpload(fileBinary).then(res => {
          return resolve(res.data)
        }).catch(err => {
          return reject(err)
        })
      })
    }

    // 分片上传
    const uploadByPieces = async (file, onProgress) => {
      const task = await getTaskInfo(file)
      if (task) {
        const { finished, taskRecord, documentFile } = task
        const { fileIdentifier: identifier } = taskRecord
        if (finished) { // 如果第一次task finished为true说明之前上传过，后端直接返回上传文件form表单documentFile字段
          return documentFile
        } else {
          const errorList = await handleUpload(file, taskRecord, onProgress)
          if (errorList.length > 0) {
            ElNotification.error({
              title: '文件上传错误',
              message: '部分分片上传失败，请尝试重新上传文件'
            })
            return;
          }
          return merge(identifier)
        }
      } else {
        ElNotification.error({
          title: '文件上传错误',
          message: '获取上传任务失败'
        })
      }
    }
    const handleHttpRequest = async ({ file, onProgress }) => {
      emit('onProgress')
      try {
        // 如果文件小于10MB，直接上传
        if (file.size < 10 * 1024 * 1024) {
          const res = await upload(file)
          emit('uploadSuccess', res)
          return res
        } else { // 分片上传
          const res = await uploadByPieces(file, onProgress)
          emit('uploadSuccess', res)
          return res
        }
      } catch (e) {
        return e
      }
    }
    /**
     * 移除文件列表中的文件
     * 如果文件存在上传队列任务对象，则停止该队列的任务
     */
    const handleRemoveFile = (uploadFile, uploadFiles) => {
      const queueObject = fileUploadChunkQueue[uploadFile.uid]
      if (queueObject) {
        queueObject.stop()
        fileUploadChunkQueue[uploadFile.uid] = undefined
      }
      const fileIdList = uploadFiles.map(item => item.response.fileId)
      emit('removeUpload', fileIdList)
    }

    const myUpload = ref(null)

    const clearFiles = () => {
      myUpload.value.clearFiles()
    }

    return {
      handleUpload,
      handleHttpRequest,
      handleRemoveFile,
      myUpload,
      clearFiles,
    }
  }
}
</script>

<style scoped>
.slice-upload {
  width: 80%;
  margin: 0 auto;
}
:deep(.el-upload-list__item) {
  display: inline-flex;
}
</style>